#include "render_target_opengl.h"
#include "renderer.h"
#include "matrix_stack.h"
#include "lexical_cast.h"
#include <iostream>

using namespace krryn::system_imp;
using namespace krryn::view;
using namespace krryn::math;

class render_target_opengl::fbo_attachment{
public:
	fbo_attachment(const std::string &a_Name) : m_Name(a_Name) {}

	virtual ~fbo_attachment() {}
	virtual void init(int a_Width, int a_Height)  = 0;
	virtual void bind_to_fbo() = 0;

	virtual GLuint get_tex_id() = 0;
	std::string get_name() const { return m_Name; }
private:
	std::string m_Name;
};

namespace{
	class fbo_buffer_opengl : public render_target_opengl::fbo_attachment{
	public:
		fbo_buffer_opengl(GLenum a_AttachmentPoint, const std::string &a_Name) 
			: m_AttachmentPoint(a_AttachmentPoint), fbo_attachment(a_Name)
		{
			glGenRenderbuffersEXT(1, &m_Id);
			glGenTextures(1, &m_TexId);
		}

		virtual ~fbo_buffer_opengl(){
			glDeleteRenderbuffersEXT(1, &m_Id);
			glDeleteTextures(1, &m_TexId);
		}

		virtual void init(int a_Width, int a_Height){
			// Use depth-stencil to prevent the GPU form having to do any data-format conversions
			glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, m_Id);
			glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH24_STENCIL8_EXT, a_Width, a_Height);
			glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, 0);

			glBindTexture(GL_TEXTURE_2D, m_TexId);
			glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
			glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
			glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);
			glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8_EXT, a_Width, a_Height, 0,
				GL_DEPTH_STENCIL_EXT, GL_UNSIGNED_INT_24_8_EXT , 0);
			glBindTexture(GL_TEXTURE_2D, 0);
		}

		virtual void bind_to_fbo(){
			glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, 
				GL_RENDERBUFFER_EXT, m_Id);
			glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_TEXTURE_2D, m_TexId, 0);
		}

		virtual GLuint get_tex_id(){ return m_TexId; }
	private:
		GLuint m_Id, m_TexId;
		GLenum m_AttachmentPoint;
	};

	class fbo_texture_opengl : public render_target_opengl::fbo_attachment{
	public:
		fbo_texture_opengl(GLenum a_AttachmentPoint, const std::string &a_Name)
		: m_AttachmentPoint(a_AttachmentPoint), fbo_attachment(a_Name)
		{
			glGenTextures(1, &m_Id);
		}

		virtual ~fbo_texture_opengl(){
			glDeleteTextures(1, &m_Id);
		}

		virtual void init(int a_Width, int a_Height){
			glBindTexture(GL_TEXTURE_2D, m_Id);
			glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
			glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
			glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_FALSE);

			// GL_UNSIGNED_INT_8_8_8_8_REV to prevent data format conversions
			glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, a_Width, a_Height, 0, GL_RGBA, 
				GL_UNSIGNED_INT_8_8_8_8_REV, 0);
			glBindTexture(GL_TEXTURE_2D, 0);
		}

		virtual void bind_to_fbo(){
			glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, 
				m_AttachmentPoint, 
				GL_TEXTURE_2D, m_Id, 0);
		}

		virtual GLuint get_tex_id(){ return m_Id; }
	private:
		GLuint m_Id;
		GLenum m_AttachmentPoint;
	};
}

render_target_opengl::render_target_opengl(int a_Width, int a_Height) : render_target(a_Width, a_Height){
	glGenFramebuffersEXT(1, &m_FboId);
	m_ColorAttachmentCount = 0;
	m_HasDepthAttachment = false;
}

render_target_opengl::~render_target_opengl(){
	glDeleteFramebuffersEXT(1, &m_FboId);
}

void render_target_opengl::attach(target_type a_Type, const std::string &a_Name){
	glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, m_FboId);

	fbo_attachment *l_Attachment = create_attachment(a_Type, a_Name);

	l_Attachment->init(m_Width, m_Height);
	l_Attachment->bind_to_fbo();

	std::cout << "Attaching: " 
		<< ((a_Type == target_rgba) ? "rgba" : "depth")
		<< " " << a_Name;

	m_Attachments.push_back(l_Attachment);
	m_NameToAttachment[a_Name] = l_Attachment;

	glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
}

void render_target_opengl::start_render(){
	glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, m_FboId);
	
	// Don't update color buffers when we don't have them
	//if(m_HasDepthAttachment && m_ColorAttachmentCount == 0){
	//	glDrawBuffer(GL_NONE);
	//}else{
	//	glDrawBuffer(GL_BACK);
	//}

	std::vector<GLenum> l_Buffers;

	for(int i = 0; i < m_ColorAttachmentCount; i++)
		l_Buffers.push_back(GL_COLOR_ATTACHMENT0_EXT + i);

	glDrawBuffersARB(l_Buffers.size(), &l_Buffers[0]);

	GLenum l_Status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT);
	if(l_Status != GL_FRAMEBUFFER_COMPLETE_EXT){
		std::string l_Error = get_fbo_error_string(l_Status);
		l_Error += " " + core::lexical_cast<std::string>(m_ColorAttachmentCount);
		std::cout << l_Error << std::endl;
 		throw std::runtime_error("Failed to create frame buffer object: " + l_Error);
	}

	glPushAttrib(GL_VIEWPORT_BIT);
	glViewport(0,0, m_Width, m_Height);
}

void render_target_opengl::end_render(){
	glPopAttrib();
	glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
}

render_target_opengl::fbo_attachment *render_target_opengl::create_attachment(target_type a_Type, const std::string &a_Name){
	if(a_Type == target_depth_stencil){
		m_HasDepthAttachment = true;
		return new fbo_buffer_opengl(GL_DEPTH_ATTACHMENT_EXT, a_Name);
	}else{
		return new fbo_texture_opengl(GL_COLOR_ATTACHMENT0_EXT + m_ColorAttachmentCount++, a_Name);
	}
}

std::string render_target_opengl::get_fbo_error_string(GLenum status){
	switch(status){
	case GL_FRAMEBUFFER_COMPLETE_EXT: 
		return "Framebuffer complete";
	case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_EXT:
		return "Framebuffer incomplete: Attachment is NOT complete";
	case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_EXT:
		return "Framebuffer incomplete: No image is attached to FBO";
	case GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT:
		return "Framebuffer incomplete: Attached images have different dimensions";
	case GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT:
		return "Framebuffer incomplete: Color attached images have different internal formats";
	case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_EXT:
		return "Framebuffer incomplete: Draw buffer";
	case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT:
		return "Framebuffer incomplete: Read buffer";
	case GL_FRAMEBUFFER_UNSUPPORTED_EXT:
		return "Unsupported by FBO implementation";
	}
	return "Unknow error";
}

GLuint render_target_opengl::get_texture_id(const std::string &a_Name){
	return m_NameToAttachment[a_Name]->get_tex_id();
}

std::vector<std::string> render_target_opengl::get_attachment_names(){
	std::vector<std::string> l_Names;

	for(size_t i = 0; i < m_Attachments.size(); i++){
		l_Names.push_back(m_Attachments[i]->get_name());
	}

	return l_Names;
}